Add design management system to playground for evaluation and collaboration#39
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…e, save/share functionality Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
…assertions Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive design management system for the playground, enabling users to create, save, share, and manage UI schemas persistently. The implementation includes a storage service layer, a dedicated designs management page, and enhanced Studio capabilities for design lifecycle operations.
Changes:
- Added local storage-based design management service with CRUD operations and import/export functionality
- Created "My Designs" page with grid/list views, search, and per-design actions
- Enhanced Studio with save/share workflows and support for user designs, shared designs via URL, and blank canvas creation
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
services/designStorage.ts |
New storage service implementing design CRUD, sharing, import/export with localStorage backend |
pages/Studio.tsx |
Added save modal, share functionality, and routing logic for new/user/shared designs |
pages/MyDesigns.tsx |
New designs management page with search, view modes, and design operations |
pages/Home.tsx |
Added "My Designs" navigation button in header |
App.tsx |
Added /my-designs route |
| id: string; | ||
| name: string; | ||
| description?: string; | ||
| schema: any; |
There was a problem hiding this comment.
The schema field uses any type, which defeats TypeScript's type safety. Consider defining a proper schema type or using unknown with type guards to maintain type safety throughout the design storage operations.
| schema: any; | |
| schema: unknown; |
There was a problem hiding this comment.
Changed to schema: unknown for better type safety in commit 39a3854.
| private generateId(): string { | ||
| return `design_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; | ||
| } | ||
|
|
||
| private generateShareId(): string { | ||
| return Math.random().toString(36).substring(2, 14); |
There was a problem hiding this comment.
Using Date.now() and Math.random() for ID generation can produce collisions in rapid succession or concurrent operations. Consider using a more robust ID generation method like crypto.randomUUID() for collision-resistant unique identifiers.
| private generateId(): string { | |
| return `design_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; | |
| } | |
| private generateShareId(): string { | |
| return Math.random().toString(36).substring(2, 14); | |
| private generateUUID(): string { | |
| // Prefer native crypto.randomUUID when available | |
| if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { | |
| return crypto.randomUUID(); | |
| } | |
| // Fallback: generate RFC4122 v4 UUID using crypto.getRandomValues | |
| if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { | |
| const bytes = new Uint8Array(16); | |
| crypto.getRandomValues(bytes); | |
| // Per RFC4122 section 4.4 | |
| bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 | |
| bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 | |
| const toHex = (n: number) => n.toString(16).padStart(2, '0'); | |
| const segments = [ | |
| Array.from(bytes.slice(0, 4)).map(toHex).join(''), | |
| Array.from(bytes.slice(4, 6)).map(toHex).join(''), | |
| Array.from(bytes.slice(6, 8)).map(toHex).join(''), | |
| Array.from(bytes.slice(8, 10)).map(toHex).join(''), | |
| Array.from(bytes.slice(10, 16)).map(toHex).join(''), | |
| ]; | |
| return segments.join('-'); | |
| } | |
| // Last-resort fallback for environments without Web Crypto | |
| return `fallback_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; | |
| } | |
| private generateId(): string { | |
| return `design_${this.generateUUID()}`; | |
| } | |
| private generateShareId(): string { | |
| // Generate a compact share ID derived from a UUID | |
| return this.generateUUID().replace(/-/g, '').substring(0, 12); |
There was a problem hiding this comment.
Implemented cryptographically secure UUID generation using crypto.randomUUID() with fallbacks to RFC4122 v4 standard in commit 39a3854. The implementation includes proper Web Crypto API support with graceful fallbacks for older environments.
| } | ||
|
|
||
| private generateShareId(): string { | ||
| return Math.random().toString(36).substring(2, 14); |
There was a problem hiding this comment.
Share IDs generated with Math.random() are predictable and may be enumerable by malicious actors. Consider using crypto.randomUUID() or crypto.getRandomValues() for cryptographically secure share links.
| return Math.random().toString(36).substring(2, 14); | |
| // Prefer cryptographically secure IDs for shared links | |
| if (typeof crypto !== 'undefined') { | |
| if (typeof crypto.randomUUID === 'function') { | |
| return crypto.randomUUID(); | |
| } | |
| if (typeof crypto.getRandomValues === 'function') { | |
| const bytes = new Uint8Array(16); | |
| crypto.getRandomValues(bytes); | |
| return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); | |
| } | |
| } | |
| // Fallback for very old environments without Web Crypto | |
| return `share_${Date.now()}_${Math.random().toString(36).substring(2, 14)}`; |
There was a problem hiding this comment.
Implemented cryptographically secure share ID generation using Web Crypto API in commit 39a3854. Share links now use collision-resistant random IDs to prevent enumeration attacks.
apps/playground/src/pages/Studio.tsx
Outdated
| schema: JSON.parse(code) | ||
| }); | ||
| } catch (error) { | ||
| alert('Error saving design: ' + (error as Error).message); |
There was a problem hiding this comment.
Generic error alerts don't provide actionable context. Consider using a toast notification system and providing more specific error messages based on the failure type (e.g., 'Failed to save design: Storage quota exceeded').
There was a problem hiding this comment.
Replaced all alert() calls with Sonner toast notifications in commit 39a3854. Error messages now include specific context (e.g., "Failed to save design: Storage quota exceeded").
apps/playground/src/pages/Studio.tsx
Outdated
| try { | ||
| const shareUrl = designStorage.shareDesign(currentDesignId); | ||
| navigator.clipboard.writeText(shareUrl); | ||
| alert('Share link copied to clipboard!'); |
There was a problem hiding this comment.
Using browser alert() dialogs creates poor UX with blocking modals. Replace with a non-blocking toast notification component to provide feedback without interrupting user workflow.
There was a problem hiding this comment.
Replaced all alert() calls with Sonner toast notifications providing non-blocking feedback in commit 39a3854.
apps/playground/src/pages/Studio.tsx
Outdated
| alert('Error sharing design: ' + (error as Error).message); | ||
| } | ||
| } else { | ||
| alert('Please save the design first before sharing.'); |
There was a problem hiding this comment.
This alert provides instruction but blocks the UI. Consider automatically triggering the save modal instead of showing an alert, providing a smoother user experience.
| alert('Please save the design first before sharing.'); | |
| setShowSaveModal(true); |
There was a problem hiding this comment.
Changed to automatically trigger save modal with an info toast notification in commit 39a3854, providing smoother UX without blocking dialogs.
apps/playground/src/pages/Studio.tsx
Outdated
| {/* Top Header injected into Designer Context */} | ||
| <StudioToolbarContext | ||
| exampleTitle={exampleId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')} | ||
| exampleTitle={exampleId === 'new' ? 'New Design' : (typeof exampleId === 'string' ? exampleId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') : 'Untitled')} |
There was a problem hiding this comment.
This complex inline expression for title formatting reduces readability. Extract this logic into a helper function formatDesignTitle(exampleId: string) to improve code clarity and maintainability.
There was a problem hiding this comment.
Extracted title formatting logic into helper function formatDesignTitle(exampleId: string) in commit 39a3854, improving code clarity and maintainability.
| }; | ||
|
|
||
| const handleDelete = (id: string) => { | ||
| if (confirm('Are you sure you want to delete this design?')) { |
There was a problem hiding this comment.
The native confirm() dialog provides poor UX with blocking behavior and limited styling. Replace with a custom confirmation modal that matches the application's design system and allows for better messaging (e.g., showing design name in confirmation).
There was a problem hiding this comment.
Replaced native confirm() with custom confirmation modal in commit 39a3854. The modal now displays the design name and matches the application's design system.
| loadDesigns(); | ||
| navigate(`/studio/${cloned.id}`); | ||
| } catch (error) { | ||
| alert('Error cloning design: ' + (error as Error).message); |
There was a problem hiding this comment.
Multiple alert() calls throughout this file create inconsistent error handling. Implement a centralized toast notification system to provide consistent, non-blocking user feedback across all operations.
There was a problem hiding this comment.
Implemented centralized Sonner toast notification system in commit 39a3854, replacing all alert() calls with consistent, non-blocking user feedback across all operations.
| const url = URL.createObjectURL(blob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = `design-${id}.json`; |
There was a problem hiding this comment.
The exported filename uses the internal design ID which is not user-friendly. Consider using the design name (sanitized) instead: a.download = ${sanitizeFilename(design.name)}.json`` to make exported files more recognizable.
There was a problem hiding this comment.
Updated export functionality in commit 39a3854 to use sanitized design names (e.g., "My-Dashboard.json") instead of internal IDs, making exported files more recognizable.
|
✅ All checks passed!
|
…类型为 Record<string, unknown>
|
✅ All checks passed!
|
|
✅ All checks passed!
|
|
@copilot fix |
Introduces a new Page layout component with corresponding PageSchema type for top-level page containers. Updates the Home page UI to include 'New Design' and 'Start New Design' actions, and registers the PageRenderer in the component registry.
|
✅ All checks passed!
|
…ns, and confirmation dialog Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
PageRenderer now accepts an optional className and additional props, improving flexibility for custom styling and component extension.
…github.com/objectstack-ai/objectui into copilot/refactor-playground-for-evaluation
|
✅ All checks passed!
|
Added 'lint', 'test', and 'type-check' scripts to package.json files for designer, plugin-charts, plugin-editor, plugin-kanban, and plugin-markdown packages to standardize development workflows. Refactored ChartContainerImpl.tsx for improved type safety and className handling. Removed 'Studio' link from docs navigation.
…-kanban 和 @object-ui/plugin-markdown 添加基础测试
|
✅ All checks passed!
|
|
✅ All checks passed!
|
|
✅ All checks passed!
|
- #39 showRecordCount: conditionally show/hide record count bar - #24 rowHeight: add short and extra_tall mapping in ListView + bridge - #7 sort: parse legacy string format "field desc" - #22 description: render view description below toolbar - #40 allowPrinting: add print button with window.print() - #31 virtualScroll: forward flag to grid view schema - #35 userActions: wire sort/search/filter/rowHeight to toolbar visibility - #38 addRecord: render "+ Add Record" button from spec config - #37 tabs: render tab bar UI for view tabs - #9 filterableFields: restrict FilterBuilder to whitelist fields - #8 searchableFields: scope search queries to specified fields - #36 appearance: wire showDescription and allowedVisualizations - #16 pageSizeOptions: add page size selector UI in status bar - #17-21: use spec kanban/calendar/gantt/gallery/timeline configs - #20 gallery: add typed GalleryConfig to ListViewSchema - #21 timeline: add typed TimelineConfig to ListViewSchema - Bridge: add short/extra_tall density mapping, filterableFields pass-through Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>



Playground Refactoring - Complete ✅
Goal
Refactor the playground to enable users to:
Implementation Summary
This PR successfully refactors the entire playground application with comprehensive design management capabilities.
🎯 Core Features Implemented
Design Storage Service (
designStorage.ts)My Designs Page
Enhanced Studio
Home Page Updates
Code Quality Improvements
✅ All code review feedback addressed:
schema: anytoschema: unknownfor better type safetycrypto.randomUUID()with fallbacksalert()calls with Sonner toast notificationsconfirm()with custom confirmation modalformatDesignTitle()Screenshots
My Designs Management:

Save Modal:

Studio with New Design:

Import JSON:

Technical Details
Modified Files:
apps/playground/src/services/designStorage.ts- Enhanced with UUID generation and filename sanitizationapps/playground/src/pages/Studio.tsx- Added toast notifications and helper functionsapps/playground/src/pages/MyDesigns.tsx- Added confirmation dialog and toast notificationsapps/playground/src/App.tsx- Added Toaster componentapps/playground/package.json- AddedsonnerdependencyNew Dependencies:
sonner- Modern toast notification libraryUser Experience Improvements
Architecture Notes
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.